/*******************************************************************************
 * Copyright (c) 2000, 2013 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.javaeditor;

import org.eclipse.che.jdt.dom.ASTNodes;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CheASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.ui.SharedASTProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * @author Evgen Vidolob
 */
public class ASTProvider {
    public static final  int     SHARED_AST_LEVEL              = AST.JLS8;
    public static final  boolean SHARED_AST_STATEMENT_RECOVERY = true;
    public static final  boolean SHARED_BINDING_RECOVERY       = true;
    private static final Logger  LOG                           = LoggerFactory.getLogger(ASTProvider.class);
    private static final String  DEBUG_PREFIX                  = "ASTProvider > "; //$NON-NLS-1$
    private Object fReconcileLock= new Object();
    private CompilationUnit fAST;
    /**
     * Tells whether this class is in debug mode.
     * @since 3.0
     */
    private static final boolean DEBUG                         = false;

    /**
     * Checks whether the given Java element has accessible source.
     *
     * @param je the Java element to test
     * @return <code>true</code> if the element has source
     * @since 3.2
     */
    private static boolean hasSource(ITypeRoot je) {
        if (je == null || !je.exists())
            return false;

        try {
            return je.getBuffer() != null;
        } catch (JavaModelException ex) {
            LOG.error(ex.getMessage(), ex);
        }
        return false;
    }

    /**
     * Returns a string for the given Java element used for debugging.
     *
     * @param javaElement the compilation unit AST
     * @return a string used for debugging
     */
    private String toString(ITypeRoot javaElement) {
        if (javaElement == null)
            return "null"; //$NON-NLS-1$
        else
            return javaElement.getElementName();

    }

    /**
     * Returns a string for the given AST used for debugging.
     *
     * @param ast the compilation unit AST
     * @return a string used for debugging
     */
    private String toString(CompilationUnit ast) {
        if (ast == null)
            return "null"; //$NON-NLS-1$

        List<AbstractTypeDeclaration> types= ast.types();
        if (types != null && types.size() > 0)
            return types.get(0).getName().getIdentifier() + "(" + ast.hashCode() + ")"; //$NON-NLS-1$//$NON-NLS-2$
        else
            return "AST without any type"; //$NON-NLS-1$
    }

    /**
     * Creates a new compilation unit AST.
     *
     * @param input the Java element for which to create the AST
     * @param progressMonitor the progress monitor
     * @return AST
     */
    public static CompilationUnit createAST(final ITypeRoot input, final IProgressMonitor progressMonitor) {
        if (!hasSource(input))
            return null;

        if (progressMonitor != null && progressMonitor.isCanceled())
            return null;

        final CheASTParser parser = CheASTParser.newParser(SHARED_AST_LEVEL);
        parser.setResolveBindings(true);
        parser.setStatementsRecovery(SHARED_AST_STATEMENT_RECOVERY);
        parser.setBindingsRecovery(SHARED_BINDING_RECOVERY);
        parser.setSource(input);

        if (progressMonitor != null && progressMonitor.isCanceled())
            return null;

        final CompilationUnit root[]= new CompilationUnit[1];

        SafeRunner.run(new ISafeRunnable() {
            public void run() {
                try {
                    if (progressMonitor != null && progressMonitor.isCanceled())
                        return;
                    if (DEBUG)
                        System.err.println(getThreadName() + " - " + DEBUG_PREFIX + "creating AST for: " +
                                           input.getElementName()); //$NON-NLS-1$ //$NON-NLS-2$
                    root[0] = (CompilationUnit)parser.createAST(progressMonitor);

                    //mark as unmodifiable
                    ASTNodes.setFlagsToAST(root[0], ASTNode.PROTECT);
                } catch (OperationCanceledException ex) {
                    return;
                }
            }

            public void handleException(Throwable ex) {
                LOG.error(ex.getMessage(), ex);
            }
        });
        return root[0];
    }

    private static String getThreadName() {
        String name= Thread.currentThread().getName();
        if (name != null)
            return name;
        else
            return Thread.currentThread().toString();
    }

    /**
     * Returns a shared compilation unit AST for the given Java element.
     * <p>
     * Clients are not allowed to modify the AST and must synchronize all access to its nodes.
     * </p>
     *
     * @param input the Java element, must not be <code>null</code>
     * @param waitFlag {@link SharedASTProvider#WAIT_YES}, {@link SharedASTProvider#WAIT_NO} or
     *            {@link SharedASTProvider#WAIT_ACTIVE_ONLY}
     * @param progressMonitor the progress monitor or <code>null</code>
     * @return the AST or <code>null</code> if the AST is not available
     */
    public CompilationUnit getAST(final ITypeRoot input, SharedASTProvider.WAIT_FLAG waitFlag, IProgressMonitor progressMonitor) {
        if (input == null || waitFlag == null)
            throw new IllegalArgumentException("input or wait flag are null"); //$NON-NLS-1$

        if (progressMonitor != null && progressMonitor.isCanceled())
            return null;

        boolean isActiveElement;
        synchronized (this) {
            isActiveElement= false; //input.equals(fActiveJavaElement);
            if (isActiveElement) {
                if (fAST != null) {
                    if (DEBUG)
                        System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "returning cached AST:" + toString(fAST) + " for: " + input.getElementName()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

                    return fAST;
                }
                if (waitFlag == SharedASTProvider.WAIT_NO) {
                    if (DEBUG)
                        System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "returning null (WAIT_NO) for: " + input.getElementName()); //$NON-NLS-1$ //$NON-NLS-2$

                    return null;

                }
            }
        }

        final boolean canReturnNull= waitFlag == SharedASTProvider.WAIT_NO || (waitFlag == SharedASTProvider.WAIT_ACTIVE_ONLY && !(isActiveElement && fAST == null));
        boolean isReconciling= false;
        final ITypeRoot activeElement;
        if (isActiveElement) {
//            synchronized (fReconcileLock) {
//                activeElement= fReconcilingJavaElement;
//                isReconciling= isReconciling(input);
//                if (!isReconciling && !canReturnNull)
//                    aboutToBeReconciled(input);
//            }
        } else
            activeElement= null;

        if (isReconciling) {
            // Wait for AST
//                synchronized (fWaitLock) {
//                    if (isReconciling(input)) {
//                        if (DEBUG)
//                            System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "waiting for AST for: " + input.getElementName()); //$NON-NLS-1$ //$NON-NLS-2$

//                        fWaitLock.wait(30000); // XXX: The 30 seconds timeout is an attempt to at least avoid a deadlock. See https://bugs.eclipse.org/366048#c21
//                    }
//                }

            // Check whether active element is still valid
//                synchronized (this) {
//                    if (activeElement == fActiveJavaElement && fAST != null) {
//                        if (DEBUG)
//                            System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "...got AST: " + toString(fAST) + " for: " + input.getElementName()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
//
//                        return fAST;
//                    }
//                }
            return getAST(input, waitFlag, progressMonitor);
        } /*else if (canReturnNull)
            return null;*/


        CompilationUnit ast= null;
        try {
            ast= createAST(input, progressMonitor);
            if (progressMonitor != null && progressMonitor.isCanceled()) {
                ast= null;
                if (DEBUG)
                    System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "Ignore created AST for: " + input.getElementName() + " - operation has been cancelled"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
        } finally {
            if (isActiveElement) {
                if (fAST != null) {
                    // in the meantime, reconcile created a new AST. Return that one
                    if (DEBUG)
                        System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "Ignore created AST for " + input.getElementName() + " - AST from reconciler is newer"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    reconciled(fAST, input, null);
                    return fAST;
                } else
                    reconciled(ast, input, null);
            }
        }
        return ast;
    }

    /**
     * Update internal structures after reconcile.
     *
     * @param ast the compilation unit AST or <code>null</code> if the working copy was consistent
     *            or reconciliation has been cancelled
     * @param javaElement the Java element for which the AST was built
     * @param progressMonitor the progress monitor
     * @see org.eclipse.jdt.internal.ui.text.java.IJavaReconcilingListener#reconciled(CompilationUnit,
     *      boolean, IProgressMonitor)
     */
    void reconciled(CompilationUnit ast, ITypeRoot javaElement, IProgressMonitor progressMonitor) {
//        if (DEBUG)
//            System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "reconciled: " + toString(javaElement) + ", AST: " + toString(ast)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
//
//        synchronized (fReconcileLock) {
//            fIsReconciling= false;
//            if (javaElement == null || !javaElement.equals(fReconcilingJavaElement)) {
//
//                if (DEBUG)
//                    System.out.println(getThreadName() + " - " + DEBUG_PREFIX + "  ignoring AST of out-dated editor"); //$NON-NLS-1$ //$NON-NLS-2$
//
//                // Signal - threads might wait for wrong element
//                synchronized (fWaitLock) {
//                    fWaitLock.notifyAll();
//                }
//
//                return;
//            }
//            cache(ast, javaElement);
//        }
    }

}
